package raytracing;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.imageio.ImageIO;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;

import lib.common.ColorOperation;
import lib.common.Constants;
import model.Scene;
import model.lights.Light;
import model.primitive.Intersection;
import model.primitive.IntersectionData;
import model.primitive.Primitive;
import model.primitive.TriangleOld;
import model.ray.Bouncer;
import model.ray.Ray;
import model.shader.GlassShader;
import model.shader.MirrorShader;
import model.shader.Shader;
import model.tree.AdaptiveSuperSampligQuadtree;
import model.tree.AdaptiveSuperSampling;
import model.tree.OctTree;
import model.tree.OctTreeImpl;
import camera.Camera;
import camera.ThinlensCamera;
import dof.DephtOfField;

public class RayTracer implements Runnable {
	public boolean OCTTREE_ENABLED = true;		
	public boolean MULTITHREAD_ENABLED = false;
	public boolean PROGRESS_ENABLED = false;
	public boolean SOFT_SHADOW_ENABLED = true;
	public int RAYS_FOR_DOF_DEFAULT = 4;
	public double DOF; 
	
	private boolean aaEnabled = false;
	
	//Sirve si no esta definido el parametro buckets
	private int CANT_THREADS_DEFAULT = 2;			
	private int PROGRESS_GRANULARITY = 10;		

	public int width;
	public int height;

	public TraceDepth tdepth = new TraceDepth(1, 4, 4);
	public Scene scene;
	public OctTree octTree;

	public Double progreso = 0.0;
	
	// for gui version
	private BufferedImage buf;
	
	// TODO Auto-generated method stub
	public static Set<TriangleOld> triOn = new HashSet<TriangleOld>();
	public static Map<TriangleOld, Ray> triOff = new HashMap<TriangleOld, Ray>();
	public static Set<TriangleOld> triDiv = new HashSet<TriangleOld>();
	
	
	
	public RayTracer(Scene scene, int width, int height, TraceDepth traceDepth) {
		this.scene = scene;
		this.width = width;
		this.height = height;
		this.aaEnabled = scene.isAAEnabled();
		if (tdepth == null) {
			this.tdepth = new TraceDepth(1, 4, 4);	
		}
		this.octTree = new OctTreeImpl(scene);
	}

	public BufferedImage Render() {
		int rows_by_thread;
		int cant_threads = 1;
		BufferedImage image = new BufferedImage( width, height, BufferedImage.TYPE_INT_RGB );
		
		// put a grid lights per light on the scene
		if (SOFT_SHADOW_ENABLED ) {
			List<Light> newLights = new ArrayList<Light>();
			Iterator<Light> it = scene.lights.iterator();
			while (it.hasNext()) {
				for (Light l: it.next().gridLight) {
					newLights.add(l);
				}
			}
			scene.lights = newLights;
		}
		
		if( MULTITHREAD_ENABLED ) {
			if( scene.bucket != 0 ) 
				rows_by_thread = scene.bucket;
			else
				rows_by_thread = height / CANT_THREADS_DEFAULT;
			cant_threads = (int) Math.ceil(height / (double)rows_by_thread);			
			Thread threads[] = new Thread[cant_threads];

			int rows_to_compute = rows_by_thread;

			for (int i = 0; i < cant_threads; i++) {
				if( i == cant_threads - 1 )	{
					rows_to_compute = height - (cant_threads-1) * rows_by_thread;
				}
				TraceRayRunnable tracerThread = new TraceRayRunnable(i * rows_by_thread, rows_to_compute, image);				
				threads[i] = new Thread(tracerThread);
				threads[i].start();
			}
			
			for (int i = 0; i < cant_threads; i++) {
				try {
					threads[i].join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		} 
		else	{	//NO hay multithreading
			//TODO
			TraceRayRunnable tracerThread = new TraceRayRunnable(0, height, image);
//			TraceRayRunnable tracerThread = new TraceRayRunnable(117, 1, image);
			Thread thread = new Thread(tracerThread);
			thread.start();
			try {
				thread.join();
			} catch (InterruptedException e) { e.printStackTrace(); }
		}
		buf = image;

		//TODO Debbug
		for( TriangleOld triangle: triOn ){
			if( triOff.containsKey(triangle))
				triOff.remove(triangle);
			if(triDiv.contains(triangle))
				triDiv.remove(triangle);
		}
		
		for( TriangleOld t: triOff.keySet() ){
			if(triDiv.contains(t))
				triDiv.remove(t);
		}

		System.out.println( "Tri-On: " + triOn.size());
		System.out.println( "Tri-Off: " + triOff.size());
		System.out.println( "Tri-Div: " + triDiv.size());
		

		List<Point3d> p= new ArrayList<Point3d>();
		List<Vector3d> n= new ArrayList<Vector3d>();
		

		for(TriangleOld triangle: triOff.keySet() ){
			
			p.add(triangle.getVertex()[0]);
			p.add(triangle.getVertex()[1]);
			p.add(triangle.getVertex()[2]);
			n.add(triangle.normals[0]);
			n.add(triangle.normals[1]);
			n.add(triangle.normals[2]);
		}
		
//		Imprime archivo de salida
//TODO		
//		System.out.println( "------------------------------------- ");
//		
//		System.out.println( " points " + p.size() );
//		
//		for( Point3d ap: p )
//			System.out.println( " " + ap.x + " " + ap.y + " " + ap.z );
//		
//		System.out.println(" triangles " +  triOff.size() );
//		
//		for(TriangleOld t: triOff )
//			System.out.println( " " + 	p.indexOf(t.getVertex()[0]) +  " " +
//										p.indexOf(t.getVertex()[1]) +  " " +
//										p.indexOf(t.getVertex()[2])				);
//		 
//		System.out.println( " normals vertex");
//		
//		for( Vector3d an: n )
//			System.out.println( " " + an.x + " " + an.y + " " + an.z );
//
//		System.out.println( " uvs none");
		
		return image;
	}	
	
	public int getColor(Ray ray, int[] depths) {
		Color c, refl, refr;
		IntersectionData istate;
		Primitive primitive = null;
		istate = traceRay(ray, scene);
		
		if( istate != null)	{
			primitive = istate.primitive;
		}
		
		if ( primitive != null ) {
			
			Shader sh = primitive.getShader();
			//TODO: ver com mejorar esto, raytracer no tendria porq saber que el getColor
			// del mirrorShader no es el de reflejo... por otro lado, tmp es factible q mirrorshader
			// calcule su color pues para eso necesita recibir la escena y nivel de recursionq son ajenos a el
			if (!(sh instanceof MirrorShader) && !(sh instanceof GlassShader)) { // hacer un or con glasshader)
				
				c = sh.getColor(scene.getLights(), istate, this);
			} else {
				c = Constants.DEF_COLOR;
			}
		
			
			if (sh.getReflectionConstant() > 0) {
				
				if (depths[1] > 0) {
					--depths[1];
					refl = doReflection(sh.getColor(scene.getLights(), istate, this), istate, depths);
					c = ColorOperation.addColors(c, refl);
					
				}
			}
			
			if (sh.getRefractionConstant() > 0) {
				if (depths[2] > 0) {
					--depths[2];
					
					refr = doRefraction(c, istate, depths);
					//TODO: BEER's LAW
					c = ColorOperation.addColors(c, refr);
					
				}
				
			}
			
			return c.getRGB();
		} 
		return Constants.DEF_COLOR.getRGB();
	}
	
	public IntersectionData traceRay(Ray ray, Scene scene) {
		List<IntersectionData> intersections = null;
		//TODO
		if( OCTTREE_ENABLED )
			intersections = octTree.getIntersections(ray);
		else {
			Intersection intersection = new Intersection(ray, scene);
			if( intersection != null )
				intersections = intersection.hits;
		}
		if( intersections != null && intersections.size() > 0) {
			return intersections.get(0);
		}
		return null;
	}
	

	private Color doReflection(Color baseColor, IntersectionData state, int[] depth) {
		Ray reflection;
		Color ans;
		double refl;
		
		if (depth[1] > 0) {
			refl = state.primitive.getShader().getReflectionConstant();
			reflection = Bouncer.reflectRay(state);
			
			if (reflection != null) {
				ans = new Color(getColor(reflection, depth));
				ans = ColorOperation.blendColors(baseColor, ans);
				ans = ColorOperation.multiplyColor(ans,refl);
			} else {
				ans = Constants.DEF_COLOR;
			}
		} else {
			ans = Constants.DEF_COLOR;
		}
		
		return ans;
	}
	
	private Color doRefraction(Color baseColor, IntersectionData state, int[] depths) {
		Ray refraction;
		Color ans;
		
		if (depths[2] > 0) {
			refraction = Bouncer.refract(state);
			
			if (refraction == null) {
				return Constants.DEF_COLOR;
			}

			
			ans = new Color (getColor(refraction,depths));
			return ans;
		}
		return Constants.DEF_COLOR;
	}


	public static void savePNG( final BufferedImage bi, final String path ){
        try {
            RenderedImage rendImage = bi;
            ImageIO.write(rendImage, "PNG", new File(path));
        } catch ( IOException e) {
            e.printStackTrace();
        }
	}
	
	public static void saveBMP( final BufferedImage bi, final String path ){
        try {
            RenderedImage rendImage = bi;
            ImageIO.write(rendImage, "BMP", new File(path));
        } catch ( IOException e) {
            e.printStackTrace();
        }
	}
	
	class TraceRayRunnable implements Runnable {
		int startingRow;
		int rowsToCompute;
		BufferedImage image;

		public TraceRayRunnable(int startingRow, int rowsToCompute, BufferedImage image ) {
			this.startingRow = startingRow;
			this.rowsToCompute = rowsToCompute;
			this.image = image;
		}
		
		@Override
		public void run() {
			Camera camera = scene.getCamera();
			Vector3d cameye;
			
			cameye = camera.eye;
			
		  	Point3d origin = new Point3d(cameye.x, cameye.y, cameye.z);
		  	Vector3d widthReference = new Vector3d();	  	
		  	Vector3d heightReference = new Vector3d();
			Vector3d direction = new Vector3d();
			
			Ray ray = new Ray(origin, null);
			
		  	widthReference.sub(camera.TR_vector(), camera.TL_vector()); 
		  	widthReference.scale(1.0/width);	  
		  	heightReference.sub(camera.BL_vector(), camera.TL_vector());
		  	heightReference.scale(1.0/height);
			
		  	//Arranco setendo actualheight como el vector TL
		  	Vector3d actualHeight = new Vector3d(camera.TL_vector());
			Vector3d actualWidth = new Vector3d();
			
			Vector3d aux = new Vector3d();
			aux.set(heightReference);
			aux.scale(startingRow);
			actualHeight.add(aux);
			
			int colorRGB;
			int row_counter = 0;
			
			for ( int y = startingRow; y < rowsToCompute + startingRow; y++ ){  
				actualWidth.set(actualHeight);
				for ( int x = 0; x < width; x++ ){
					direction.set(actualWidth);
					direction.normalize();

					ray.setDirection(direction);
					
					if( camera instanceof ThinlensCamera )	{					//Hay DOF
						DephtOfField dephtOfField = new DephtOfField( ray, RayTracer.this );
						colorRGB = dephtOfField.getPixelValue();
					} else {				//Pinhole Camera
						if( aaEnabled )	{
							AdaptiveSuperSampling adaptiveSuperSampling = new AdaptiveSuperSampligQuadtree(3, ray, widthReference, heightReference, tdepth, RayTracer.this );
							colorRGB = adaptiveSuperSampling.pixelValue().getRGB();
						} else {
							colorRGB = getColor(ray, tdepth.getDepths());
						}
					}
					image.setRGB(x, y, colorRGB);
					actualWidth.add(widthReference);
				}
				actualHeight.add(heightReference);
//Para calcular el progreso.
				row_counter++;
				if( (row_counter % PROGRESS_GRANULARITY) == 0 )	{
					synchronized (progreso) {
						progreso += PROGRESS_GRANULARITY / (double)height * 100;
						if( PROGRESS_ENABLED ) {
							System.out.printf( "%.02f %%\n", progreso );
						}
					}
				}
//
			}
			
		}
	}

	@Override
	public void run() {
		buf = Render();
	}

	public BufferedImage getBuf() {
		return buf;
	}

	public synchronized Double getProgreso() {
		return progreso;
	}
	
	
}


